﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Data.Schema.ScriptDom.Sql;
using System.Text.RegularExpressions;
using System.Globalization;

namespace SQLite2CELib
{
    internal class WhereClauseConverter
    {
        private string _linqInPredicateQuery = "({0}).Contains({1}) == true";
        private string _linqNotInPredicateQuery = "({0}).Contains({1}) == false";
        private string _linqBetweenQuery = "{0} >= {1} && {0} <= {2}"; //0 = first expression in Between operator, 1 is second expression and, 2 is third expression

        private string _linqNotNullCondition = "object.Equals({0}, null) == false";
        private string _linqNullCondition = "object.Equals({0}, null) == true";

        private StringBuilder _whereClause;
        private TableSchemaHelper _schemaHelper;
        private Utils _utils;
        private Errors _errors;

        public WhereClauseConverter(TableSchemaHelper schemaHelper, Utils utils, Errors errors)
        {
            _schemaHelper = schemaHelper;
            _utils = utils;
            _errors = errors;
        }

        public StringBuilder Convert(WhereClause whereClause)
        {
            List<KeyValuePair<String, PropertyDetails>> propertyInfoForColumns;

            _whereClause = new StringBuilder();
            _whereClause.Append(" where ");
            if (whereClause.SearchCondition is InPredicate)
            {
                _whereClause.Append(GenerateLinqForInPredicate(whereClause.SearchCondition as InPredicate));
            }
            else
            {
                propertyInfoForColumns = new List<KeyValuePair<string, PropertyDetails>>();
                ExtractWhereClause(whereClause.SearchCondition, ref _whereClause, ref propertyInfoForColumns);
            }

            return _whereClause;
        }

        private void ExtractWhereClause(Expression whereClauseExpression,
                                        ref StringBuilder whereClause,
                                        ref List<KeyValuePair<String, PropertyDetails>> propertyInfoForColumns)
        {
            Errors errors;
            BuildLinqForSelectStatement linqBuilderForSubQuery;
            GeneratedLinqInfo linqInfoForSubQuery;
            KeyValuePair<String, PropertyDetails> matchingClassNameAndTypeInfo;
            Expression lhsExp, rhsExp;
            Type dotNetTypeForLiteralExp;
            BinaryExpression whereClauseBinaryExpression = null;
            ParenthesisExpression whereClauseParenthesisExpression = null;

            if (whereClauseExpression is ParenthesisExpression)
            {
                whereClauseParenthesisExpression = whereClauseExpression as ParenthesisExpression;
                whereClause.Append("(");
                ExtractWhereClause(whereClauseParenthesisExpression.Expression, ref whereClause, ref propertyInfoForColumns);
                whereClause.Append(")");
                return;
            }
            if (whereClauseExpression is Column)
            {
                whereClause.Append(_utils.ExtractValFromColumn(whereClauseExpression as Column, out matchingClassNameAndTypeInfo));
                propertyInfoForColumns.Add(matchingClassNameAndTypeInfo);
                return;
            }
            if (whereClauseExpression is InPredicate)
            {
                whereClause.Append(" " + GenerateLinqForInPredicate(whereClauseExpression as InPredicate));
                return;
            }
            if (whereClauseExpression is Literal)
            {
                whereClause.Append(" " + _utils.ExtractValFromLiteral(whereClauseExpression as Literal, null));
                return;
            }
            if (whereClauseExpression is TernaryExpression)
            {
                ProcessTernaryExpression(whereClauseExpression as TernaryExpression, ref whereClause);
            }
            if (whereClauseExpression is UnaryExpression)
            {
                ProcessUnaryExpression(whereClauseExpression as UnaryExpression, ref whereClause, ref propertyInfoForColumns);
            }
            if (whereClauseExpression is BinaryExpression)
            {
                whereClauseBinaryExpression = whereClauseExpression as BinaryExpression;
            }
            else
            {
                //We are not sure how to handle anything apart from ParenthesisExpression,Column,BinaryExpression ..so leave.
                return;
            }

            if (_utils.IsExpressionCondition(whereClauseBinaryExpression) == true)
            {
                _utils.EnsureLhsExpIsColumn(whereClauseBinaryExpression.FirstExpression, whereClauseBinaryExpression.SecondExpression, out lhsExp, out rhsExp);
                whereClause.Append(_utils.ExtractValueFromExpression(lhsExp, out matchingClassNameAndTypeInfo));
                propertyInfoForColumns.Add(matchingClassNameAndTypeInfo);

                whereClause.Append("  ");
                whereClause.Append(OperatorMappings.GetMapping(whereClauseBinaryExpression.BinaryExpressionType));
                whereClause.Append("  ");

                if (whereClauseBinaryExpression.SecondExpression is Subquery)
                {
                    linqBuilderForSubQuery = new BuildLinqForSelectStatement();
                    linqInfoForSubQuery = linqBuilderForSubQuery.GenerateLinq(ExtractSelectStatementForSubQuery(whereClauseBinaryExpression.SecondExpression as Subquery),
                                                                              _schemaHelper,
                                                                              out errors);
                    if (linqInfoForSubQuery == null)
                    {
                        _errors.AddRange(errors.ErrorMessages.AsEnumerable());
                        throw new NotSupportedException("Error converting sub query");
                    }
                    whereClause.Append(String.Concat("(", linqInfoForSubQuery.Linq.ToString(), ")"));
                }
                else
                {
                    //The value of dot net type is nothing but data type of property which for RHS Exp.
                    dotNetTypeForLiteralExp = matchingClassNameAndTypeInfo.Value.TypeInfo;
                    whereClause.Append(_utils.ExtractValueFromExpression(rhsExp, out matchingClassNameAndTypeInfo, dotNetTypeForLiteralExp));
                    whereClause.Append("  ");
                    propertyInfoForColumns.Add(matchingClassNameAndTypeInfo);
                }
                return;
            }

            ExtractWhereClause(whereClauseBinaryExpression.FirstExpression, ref whereClause, ref propertyInfoForColumns);
            whereClause.Append(" " + OperatorMappings.GetMapping(whereClauseBinaryExpression.BinaryExpressionType) + " ");
            ExtractWhereClause(whereClauseBinaryExpression.SecondExpression, ref whereClause, ref propertyInfoForColumns);
        }

        private void ProcessUnaryExpression(UnaryExpression unaryExpression, ref StringBuilder whereClause, ref List<KeyValuePair<String, PropertyDetails>> propertyInfoForColumns)
        {
            String linqExpFormat;
            StringBuilder unaryExpressionString;

            unaryExpressionString = new StringBuilder();
            ExtractWhereClause(unaryExpression.Expression, ref unaryExpressionString, ref propertyInfoForColumns);

            switch (unaryExpression.UnaryExpressionType)
            {
                default:
                case UnaryExpressionType.Positive:
                case UnaryExpressionType.Not:
                case UnaryExpressionType.Negative:
                case UnaryExpressionType.BitwiseNot:
                    throw new NotSupportedException("Used Unary Expression Type Is Not Supported Yet.");
                case UnaryExpressionType.IsNotNull:
                    linqExpFormat = _linqNotNullCondition;
                    break;
                case UnaryExpressionType.IsNull:
                    linqExpFormat = _linqNullCondition;
                    break;
            }
            whereClause.AppendFormat(linqExpFormat, unaryExpressionString);
        }

        private void ProcessTernaryExpression(TernaryExpression ternaryExpression, ref StringBuilder whereClause)
        {
            FunctionCall funcInExp;
            List<KeyValuePair<String, PropertyDetails>> propertyInfoForColumns;
            StringBuilder columnInExpression, firstExpression, secondExpression;

            propertyInfoForColumns = new List<KeyValuePair<string, PropertyDetails>>();
            columnInExpression = new StringBuilder();
            ExtractWhereClause(ternaryExpression.FirstExpression, ref columnInExpression, ref propertyInfoForColumns);

            firstExpression = new StringBuilder();
            if (ternaryExpression.SecondExpression is FunctionCall)
            {
                funcInExp = ternaryExpression.SecondExpression as FunctionCall;
                if (funcInExp.FunctionName.Value.ToUpperInvariant() == "DATETIME")
                {
                    firstExpression.Append("DateTime.Parse(");
                    ExtractWhereClause(funcInExp.Parameters[0], ref firstExpression, ref propertyInfoForColumns);
                    firstExpression.Append(")");
                }
            }
            else if (ternaryExpression.SecondExpression is Expression)
            {
                ExtractWhereClause(ternaryExpression.SecondExpression, ref firstExpression, ref propertyInfoForColumns);
            }


            secondExpression = new StringBuilder();
            if (ternaryExpression.ThirdExpression is FunctionCall)
            {
                funcInExp = ternaryExpression.ThirdExpression as FunctionCall;
                if (funcInExp.FunctionName.Value.ToUpperInvariant() == "DATETIME")
                {
                    secondExpression.Append("DateTime.Parse(");
                    ExtractWhereClause(funcInExp.Parameters[0], ref secondExpression, ref propertyInfoForColumns);
                    secondExpression.Append(")");
                }
            }
            else if (ternaryExpression.SecondExpression is Expression)
            {
                ExtractWhereClause(ternaryExpression.ThirdExpression, ref secondExpression, ref propertyInfoForColumns);
            }

            whereClause.AppendFormat(_linqBetweenQuery, columnInExpression, firstExpression, secondExpression);
        }

        private String GenerateLinqForInPredicate(InPredicate inPredicate)
        {
            Errors errors;
            SelectStatement queryInsideInPredicate;
            BuildLinqForSelectStatement linqBuilderForSubQuery;
            GeneratedLinqInfo linqInfo;
            String linqWithoutAnonymousTypeInSelect;

#warning inPredicate may not always contain select statement..We need to look for a way to handle other possibilities.
            queryInsideInPredicate = ExtractSelectStatementFromInPredicate(inPredicate);
            linqBuilderForSubQuery = new BuildLinqForSelectStatement();
            linqInfo = linqBuilderForSubQuery.GenerateLinq(queryInsideInPredicate, _schemaHelper, out errors);

            if (linqInfo == null)
            {
                _errors.AddRange(errors.ErrorMessages.AsEnumerable());
                throw new NotSupportedException("Error converting sub query");
            }

            //We can safely assume that select query inside in predicate cannot return more than one column hence
            //we donot need to wrap the result of select query inside anonymous type..So we remove new {...} from the select clause of generated linq
            linqWithoutAnonymousTypeInSelect = RemoveAnonymousTypeWrapperFromSelect(linqInfo.Linq);

            KeyValuePair<String, PropertyDetails> keyValuePairs;
            Object value = _utils.ExtractValFromColumn(inPredicate.Expression as Column, out keyValuePairs);

            return GetLinqClauseForIn(linqWithoutAnonymousTypeInSelect, value.ToString(), !inPredicate.NotDefined);
        }

        private SelectStatement ExtractSelectStatementFromInPredicate(InPredicate inPredicate)
        {
            SelectStatement selectStatement;

            selectStatement = new SelectStatement()
            {
                QueryExpression = inPredicate.Subquery.QueryExpression
            };
            return selectStatement;
        }

        private SelectStatement ExtractSelectStatementForSubQuery(Subquery subQuery)
        {
            SelectStatement selectStatement;
            selectStatement = new SelectStatement()
            {
                QueryExpression = subQuery.QueryExpression
            };
            return selectStatement;
        }

        private String GetLinqClauseForIn(string subQuery, string column, bool forIn)
        {
            return forIn == true ? String.Format(CultureInfo.InvariantCulture, _linqInPredicateQuery, subQuery, column) : String.Format(CultureInfo.InvariantCulture, _linqNotInPredicateQuery, subQuery, column);
        }

        private String RemoveAnonymousTypeWrapperFromSelect(StringBuilder linqQuery)
        {
            int indexOfLastClosingBracket;
            string linqQueryWithoutWrapper;
            Regex regexToLocateSelectNew;

            //Overall logic is to replace 'select new { propertyName = value }' with 'select value'

            //Step1 - replace select new {propertyname = " with select 
            linqQueryWithoutWrapper = linqQuery.ToString();
            regexToLocateSelectNew = new Regex(@"\sselect*\snew*\s{*\s*.*\s=", RegexOptions.Multiline);
            linqQueryWithoutWrapper = regexToLocateSelectNew.Replace(linqQueryWithoutWrapper, "select");

            //Step 2 - remove closing bracket that appears at the end of the linq query.
            indexOfLastClosingBracket = linqQueryWithoutWrapper.LastIndexOf("}", StringComparison.Ordinal);
            return linqQueryWithoutWrapper.Remove(indexOfLastClosingBracket, 1);
        }
    }
}
